#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
This module defines Tag and Task objects. They will be used when passing
data to database.
"""

__all__ = [ 'Task', 'Tag' ]

import tatolib.errors as errors
import tatolib.strutil as strutil
import tatolib.timeutil as timeutil

class Task(object):
    """
    Task object. Define attributes: name, detail, due, complete, flags.
    """
    def __init__(self, **args):
        # Required attributes
        for each_req in ['name', 'detail', 'due']:
            if not args.has_key(each_req):
                raise errors.TaskObjectError('task', \
                        errors.E_TA_ATTRMISSING)
        if not timeutil.isdatetime(args['due']) or \
                not timeutil.istzaware(args['due']):
            raise errors.TaskObjectError('[tzaware,datetime]task.due',\
                                         errors.E_TA_BADVALUE)
        if not strutil.isunicode(args['name']):
            raise errors.TaskObjectError('[unicode]task.name', \
                                         errors.E_TA_BADVALUE)
        if not strutil.isunicode(args['detail']):
            raise errors.TaskObjectError('[unicode]task.detail', \
                                         errors.E_TA_BADVALUE)
        self.__name   = args['name']
        self.__detail = args['detail']
        self.__due    = timeutil.ltou(args['due'])
        # Optional attribute: flags
        self.__flags = {}
        if args.has_key('flags'):
            if type({}) is not type(args['flags']):
                raise errors.TaskObjectError('task.flags', \
                                             errors.E_TA_WRONGTYPE)
            for eachpair in args['flags'].items():
                flagname = eachpair[0]
                weighting_status = eachpair[1]
                if type(weighting_status) is not type(()):
                    raise errors.TaskObjectError(\
                            '[Tuple]flags.{0}'.format(flagname), \
                            errors.E_TA_WRONGTYPE)
                flag_weight = weighting_status[0]
                if type(flag_weight) is not type(0) or flag_weight < 0:
                    raise errors.TaskObjectError(\
                            '[int>=0]flags.{0}[0]'.format(flagname), \
                            errors.E_TA_BADVALUE)
                flag_done = weighting_status[1]
                if flag_done is not True and flag_done is not False:
                    raise errors.TaskObjectError(\
                            '[int>=0]flags.{0}[1]'.format(flagname), \
                            errors.E_TA_BADVALUE)
                # Verify string: flag name does not allow colon and
                # spaces
                if not strutil.isunicode(flagname):
                    raise errors.TaskObjectError(\
                            '[unicode]task.flags.{0}'.format(flagname), \
                            errors.E_TA_BADVALUE)
                if flagname.find(strutil.u_(' ')) >= 0:
                    raise errors.TaskObjectError(\
                            '[no space]task.flags.{0}'.format(flagname), \
                            errors.E_TA_BADVALUE)
                if flagname.find(strutil.u_(':')) >= 0:
                    raise errors.TaskObjectError(\
                            '[no colon]task.flags.{0}'.format(flagname), \
                            errors.E_TA_BADVALUE)
                # Format flags[key] = [weighting, setflag]
                self.__flags[eachpair[0]] = (eachpair[1][0],
                                             eachpair[1][1])
        # Optional attribute: complete
        if args.has_key('complete'):
            complete = args['complete']
            if complete is not None and \
                    (not timeutil.isdatetime(complete) or \
                    not timeutil.istzaware(complete)):
                raise errors.TaskObjectError(\
                        '[tzaware,datetime]task.complete', \
                        errors.E_TA_BADVALUE)
            else:
                self.__complete = complete
        else:
            self.__complete = None
        # End

    @property
    def name(self):
        "Task.name property"
        return self.__name
    @property
    def detail(self):
        "Task.detail property"
        return self.__detail
    @property
    def due(self):
        "Task.due property"
        return self.__due
    @property
    def flags(self):
        """
        Task.flags property.

        Type:\tdict,
        Key:\tName of the flag.
        Value:\tA tuple: ([int>=0]Weighting, [bool]IsFinished)
        """
        return self.__flags.copy()
    @property
    def complete(self):
        "Task.complete property"
        return self.__complete

    @name.setter
    def name(self, value):
        if not strutil.isunicode(value):
            raise errors.TaskObjectError('task.name', \
                                         errors.E_TA_WRONGTYPE)
        self.__name = value
    @detail.setter
    def detail(self, value):
        if not strutil.isunicode(value):
            raise errors.TaskObjectError('task.detail', \
                                         errors.E_TA_WRONGTYPE)
        self.__detail = value
    @due.setter
    def due(self, value):
        if not timeutil.isdatetime(value) or \
                not timeutil.istzaware(value):
            raise errors.TaskObjectError('task.due', \
                                         errors.E_TA_WRONGTYPE)
        self.__due = timeutil.ltou(value)
    @complete.setter
    def complete(self, value):
        if not timeutil.isdatetime(value) or \
                not timeutil.istzaware(value):
            raise errors.TaskObjectError('task.due', \
                                         errors.E_TA_WRONGTYPE)
        self.__complete = timeutil.ltou(value)

    def setflag(self, flag, val = True):
        """
        Task.setflag(self, flag, val = True) -> None

        Set or unset a flag defined in the task.
        """
        if not self.__flags.has_key(flag):
            raise errors.TaskObjectError(\
                    'task.flags[{0}]'.format(flag), \
                    errors.E_TA_ATTRMISSING)
        if val:
            self.__flags[flag] = (self.__flags[flag][0], True)
        else:
            self.__flags[flag] = (self.__flags[flag][0], False)
    def addflag(self, flag, weighting = 0, isset = False):
        """
        Task.addflag(self, flag, weighting = 0, isset = False) -> None

        Add a new flag to given task.

        If given flag already exists, an TaskObjectError exception
        is thrown.
        """
        if self.__flags.has_key(flag):
            raise errors.TaskObjectError(\
                    'task.flags[{0}] defined'.format(flag), \
                    errors.E_TA_ATTRMISSING)
        if weighting < 0:
            raise errors.TaskObjectError(\
                    '[int>0]task.weighting = {0}'.format(weighting), \
                    errors.E_TA_BADVALUE)
        if isset is not True and isset is not False:
            raise errors.TaskObjectError(\
                    '[bool]task.isset = {0}'.format(weighting), \
                    errors.E_TA_BADVALUE)
        self.__flags[flag] = (weighting, isset)

    def delflag(self, flag):
        """
        Task.delflag(self, flag) -> None

        Delete a flag to given task.

        Return a tuple (weighting, status) of deleted flag.

        If given flag does not exist, an TaskObjectError exception
        is thrown.
        """
        if not strutil.isunicode(flag):
            raise errors.TaskObjectError(\
                    '[unicode]flag:{0}'.format(flag), \
                        errors.E_TA_ATTRMISSING)
        if not self.__flags.has_key(flag):
            raise errors.TaskObjectError(\
                    'task.flags[{0}] not exists'.format(flag), \
                        errors.E_TA_ATTRMISSING)
        retval = self.__flags[flag]
        del self.__flags[flag]
        return retval

    def flagstostr(self):
        """
        Task.flagstostr(self) -> String of flags 

        Convert flags list to a language independent string, which is
        easy to store in database.

        Return a string of flags. If the task has no flags defined,
            returns empty string.
        """
        retstrs = []
        for each_flagpair in self.__flags.items():
            name      = each_flagpair[0]
            weighting = each_flagpair[1][0]
            status    = each_flagpair[1][1]
            if status:
                status = 1
            else:
                status = 0
            # It's safe to use colon and space here because we has 
            # prevent user naming flags with colon and space.
            retstrs.append("{0}:{1}:{2}".format(name, weighting, status))
        return " ".join(retstrs)

    @classmethod
    def strtoflags(cls, flagstr):
        """
        @classmethod
        Task.strtoflags(cls, flagstr): -> Flag dictionary

        Convert a string representation of flags to structure. The
        flag-to-string conversion is done by Task.flagstostr().

        Return a dictionary of flags
        """
        retflags = {}
        split_flagstrs = flagstr.split(' ')
        for each_flagpair in split_flagstrs:
            if each_flagpair == "":
                continue
            name_weight_status = each_flagpair.split(':')
            if len(name_weight_status) != 3:
                raise errors.TaskObjectError(\
                        '[bad format]{0}'.format(each_flagpair), \
                        errors.E_TA_BADVALUE)
            try:
                name      = name_weight_status[0]
                weighting = int(name_weight_status[1]) 
                status    = int(name_weight_status[2]) 
                if status:
                    status = True
                else:
                    status = False
                if weighting < 0:
                    raise ValueError("weighting < 0", each_flagpair)
                retflags[name] = (weighting, status)
            except Exception:
                raise errors.TaskObjectError(\
                        '[bad format]{0}'.format(each_flagpair), \
                        errors.E_TA_BADVALUE)
        return retflags

    def getweighting(self, tags):
        """
        Task.getweighting(self, tags) -> Weighting value

        Calculate weighting for a task. The most important function in
        tatolib. Used by Database objects to calculate weighting of
        given task.

        Parameter tags is a list of Tag objects, which
        represents the tags assigned to this task.

        Algorithm:
        - If given task is complete, weighting = 0
        - Every ongoing task has a basic weighting 10.
        - Add weighting of each assigned tag to basic weighting.
        - Add weighting of each unset flags from basic weighting.
        - Calculate how many days left from today to due day:
           * If today is earlier than due:
                   weighting = (today - due) * 0.2
           * If today is later than due (task is overdue),
                   weighting = (today - due) * 2
           the delta weighting will be added from basic weighting.
        - If the result is less than 0, set it to 0.
        - If the result is higher than 50, set it to 50.
        """
        if self.__complete is not None:
            return 0
        tws = [ each_tag.weighting for each_tag in tags ]

        basic_weighting = 10

        # Simply add all tag weightings together
        tag_weighting = reduce(lambda x, y: x+y, tws, 0)

        flag_weighting = 0
        for each_keyvals in self.__flags.items():
            each_flag_weighting = each_keyvals[1][0]
            each_flag_status    = each_keyvals[1][1]
            if not each_flag_status: # The flag is not set
                flag_weighting += each_flag_weighting

        days_weighting = 0
        today      = timeutil.utctoday()
        due        = self.__due
        delta_days = (today - due).days 
        if delta_days > 0: # Overdue
            days_weighting = delta_days * 2
        else:
            days_weighting = int(round(delta_days * 0.2))

        weighting = basic_weighting + tag_weighting + \
                    flag_weighting + days_weighting

        if weighting > 50:
            weighting = 50
        elif weighting < 0:
            weighting = 0

        return weighting


class Tag(object):
    """
    Task object. Define attributes: name, detail, weighting.
    """
    def __init__(self, **args):
        # Required attributes
        for each_req in ['name', 'detail', 'weighting']:
            if not args.has_key(each_req):
                raise errors.TaskObjectError('tag.{0}'.format(each_req), \
                                             errors.E_TA_ATTRMISSING)
        if not strutil.isunicode(args['name']):
            raise errors.TaskObjectError('[unicode]tag.name', \
                                         errors.E_TA_BADVALUE)
        if not strutil.isunicode(args['detail']):
            raise errors.TaskObjectError('[unicode]tag.detail', \
                                         errors.E_TA_BADVALUE)
        if type(args['weighting']) is not type(0) or \
            args['weighting'] < 0:
            raise errors.TaskObjectError('[int>=0]tag.weighting', \
                                         errors.E_TA_BADVALUE)
        self.__name      = args['name']
        self.__detail    = args['detail']
        self.__weighting = args['weighting']
        # End

    @property
    def name(self):
        "Tag.name property"
        return self.__name
    @property
    def detail(self):
        "Tag.detail property"
        return self.__detail
    @property
    def weighting(self):
        "Tag.weighting property"
        return self.__weighting

    @name.setter
    def name(self, value):
        if not strutil.isunicode(value):
            raise errors.TaskObjectError('tag.name', \
                                         errors.E_TA_WRONGTYPE)
        self.__name = value
    @detail.setter
    def detail(self, value):
        if not strutil.isunicode(value):
            raise errors.TaskObjectError('tag.detail', \
                                         errors.E_TA_WRONGTYPE)
        self.__detail = value
    @weighting.setter
    def weighting(self, value):
        if type(value) is not type(0) or value < 0:
            raise errors.TaskObjectError('[int>=0]tag.weighting', \
                                         errors.E_TA_WRONGTYPE)
        self.__weighting = value


# vim:tw=78:et:sw=4
